#
# CS 143
#
# This file provides the Link class definition.
#
# @author Joe Wang
#

import copy, random, sys, string, time
import Routing, Queue
from Packet import Packet
from SimPy.Simulation import *

class Link(Process):
    '''The Link class represents a process by which individual data packets are sent
    across the network. At the moment, it semantically encapsulates Layer 2, so if
    we wanted to implement physical links and switches later, we would be able to.

    '''
    # Metrics metric
    # What we log to, if it isn't None. Should NEVER BE NONE when running
    metrics = None


    def __init__(self, linkRate, quality, propDelay):
        '''Link class constructor.

        '''
        # Initialize parent.
        Process.__init__(self)
        
        # Set the link properties.
        
        # dict __arpTable
        # Address Resolution Protocol Table
        self.__arpTable = {}
        
        # Queue __wire
        # Temporary queue to process packets sent over the link.
        self.__wire = Queue.Queue()

        # float __linkRate
        # Link Rate
        self.__linkRate = linkRate

        # float __quality
        # Link Quality
        self.__quality = quality

        # float __propDelay
        # Link Propagation Delay
        self.__propDelay = propDelay
        
    def go(self):
        '''Link class Process Execution Method.

        '''

        # The Link's sole purpose is to transmit packets. This entails
        # taking the packets and calling the ending NIC's receive function.
        while True:

            # Send out all the packets that may exist in the queue.
            while not self.__wire.empty():
                (nics, packet, timeToDest) = self.__wire.get()

                # Wait for the transmit time.
                while timeToDest - now() > 0:
                    yield hold, self, timeToDest - now()

                # Send the packet to all the requested nics.
                for nic in nics:

                    # Simulate link quality.
                    if random.random() < self.__quality:

                        # All the planets aligned. So we send off the packet.
                        nic.receive(packet)
                    else:

                        # Log that we've dropped packet due to link quality.
                        if packet.ack:
                            print "Link failed to deliver ack %d" % packet.pid
                        else:
                            print "Link failed to deliver packet %d" % packet.pid
                        self.metrics.log(self, now(), packet, "PacketLost", nic)

            # No more packets to send just yet. So we idle.
            yield passivate, self


    def connect(self, nic):
        '''Connects the given NIC to the link.

        '''
        
        # Update the local address table.
        if not nic.getIP() in self.__arpTable:
            self.__arpTable[nic.getIP()] = nic

    def disconnect(self, nic):
        '''Disconnects the given NIC from the link.

        '''

        # Remove the entry from the ARP table.
        if nic.getIP() in self.__arpTable:
            del self.__arpTable[nic.getIP()]

        # Disconnect procedure in the nic.
        nic.disconnect()


    def send(self, packet, gateway):
        '''Sends the given packet.

        '''

        # Instrumentation
        self.metrics.log(self, now(), packet, 'LinkSending')
         
        # Calculate the time the packet will reach the destination.
        timeToDest = now() + self.__propDelay

        # Determine the NIC(s) to send via. There are multiple cases we need to
        # handle.

        # 1. The packet is in the local network.
        if packet.destination in self.__arpTable:
            self.__wire.put(([self.__arpTable[packet.destination]], packet, timeToDest))
        
        # 2. The packet is broadcast.
        elif packet.destination == (255, 255, 255, 255):

            # Prevent broadcast loopback.
            newArp = copy.copy(self.__arpTable)
            del newArp[packet.source]

            self.__wire.put((newArp.values(), packet, timeToDest))

        # 3. The packet isn't local.
        else:
            # Use the gateway specified by the NIC, if given.
            if gateway:
                self.__wire.put(([self.__arpTable[gateway]], packet, timeToDest))

        # Reactivate ourselves to continue sending.
        if self.passive():
            reactivate(self)
        
    def getLinkRate(self):
        '''Gets the link rate.

        '''
        return self.__linkRate

    def getArpTable(self):
        '''Gets the ARP table.

        '''
        return self.__arpTable

    def getPropDelay(self):
        '''Gets the link propagation delay.

        '''
        return self.__propDelay
